# -*- coding: utf-8 -*-


# cmap2owl -- Helper application to convert from concept maps to OWL ontologies
# Copyright (c) 2008-2013  Rodrigo Rizzi Starr
#  
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#  
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#  
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


'''
Reads a CMapsTools file and generates a generic representation.

Architecture: it uses input and output filters. Input filters convert a given
representation to the so called generic (or independent) representation. Output
filters convert from the independent representation to a given representation.

To better understand the independent representation, please check the UML
documentation.

@author: Rodrigo Rizzi Starr
@copyright: Copyright © 2008-2013 Rodrigo Rizzi Starr
@license: MIT License
@contact: rodrigo.starr@gmail.com
'''


import nltk

from pyswip import Atom
from phrase import Phrase


def removeLines(string):
    '''Ensure that all lines are removed, no matter if they are linux or windows
       (may not work for old Macs)
    '''
    return string.replace('\n', ' ').replace('\r', '')


class Graph:
    '''
    Holds a graph.
    '''
  
    def __init__(self, name=u''):
    
        self.name = name
        self.graphElements = {}
        self.nodes = [] # Node ids
        self.arcs = []  # Arc ids
        self.connections = [] # Holds the connections between arcs and nodes
        self.connectionMap = {} # Maps tuples to connections objects
        self._serialId = 0 # By convention, the first id is always 0
        self.phrases = [] # Holds a list of Phrase objects to help semantic
                          # analysis
        self.graphicalProperties = Point(100, 100)
    
    def _getNewId(self):
        'Returns an id that is different from the previous ones'
    
        newId = self._serialId + 1
        self._serialId = newId
    
        return self.createValidId(str(newId))
    
    def mangle(self, char):
        'Mangle the char'
        char = unicode(char)
        char = char.replace(' ', '_').replace('\n', '_').replace('\\', '_').replace('/', '_')
        return char
    
    def redefineIdsFromLabels(self):
        'Change all the ids so that they will be more similar to the labels'
        
        mapping = {}
        for el in self.getElements():
            mangle = self.mangle(el.label)
            while mangle in mapping.values():
                mangle += '_'
            
            mapping[el.id] = mangle
            
        for oldName, newName in mapping.iteritems():
            el = self.graphElements.pop(oldName)
            self.graphElements[newName] = el
            el.id = newName
            
        oldNodes = self.nodes
        oldArcs = self.arcs
        self.nodes = [mapping[key] for key in oldNodes]
        self.arcs = [mapping[key] for key in oldArcs]
    
    def createValidId(self, proposedId):
        'Mangle the proposed id in such a way that it begins with an alpha'
        
        mangle = ''
        if isinstance(proposedId, Atom):
            return str(proposedId)
        if not (isinstance(proposedId, basestring) or 
                proposedId[0].isalpha() or 
                proposedId[0] == "'"):
            mangle = 'X'
    
        return mangle + proposedId.replace('\\', '\\\\')
    
    def getElementById(self, elId):
        'Returns an element with id'
        
        try:
#            print('class %s: %s' % (elId.__class__.__name__, elId))
            return self.graphElements[self.createValidId(elId)]
        except KeyError:
            return None
        except:
            raise
    
    def getElements(self):
        'Return an iterable over each element'
        for el in self.graphElements.itervalues():
            if not isinstance(el, Connection):
                yield el
            
    def getConnections(self):
        'Return an iterable over the connections'
        for el in self.graphElements.itervalues():
            if isinstance(el, Connection):
                yield el
            
    def getNodes(self):
        'Return an iterable over each node'
        for nodeId in self.nodes:
            yield self.getElementById(nodeId)
            
    def getLinkingPhrases(self):
        'Return an iterable over each arc'
        for arcId in self.arcs:
            yield self.getElementById(arcId)
    
    def addNode(self, label, elId=None):
        'Add a new node to the graph'
    
        node = Node(self, elId)
    
        node.label = label
    
        self.graphElements[node.id] = node
        self.nodes.append(node.id)
    
        return node

    def addArc(self, label, elId=None):
        'Add a new arc to the graph'
    
        arc = Arc(self, elId)
    
        arc.label = label
    
        self.graphElements[arc.id] = arc
        self.arcs.append(arc.id)
    
        return arc

    def connect(self, elFrom, to, connectionId):
        'Connect to elements'
        elFrom._ConnectTo(to)
        
        connection = Connection(self, connectionId)
        self.graphElements[connection.id] = connection        
        self.connections.append(connection)
        self.connectionMap[ (elFrom, to) ] = connection
        return connection

    def getStatistics(self):
        '''Returns some statistics about the graph
        '''
        statistics = {}

        statistics['Nodes'] = len(self.nodes)
        statistics['Arcs'] = len(self.arcs)
        statistics['Connections'] = len(self.connections)
        
        statistics['Avg LP degree'] = (sum(len(arc.inbound) + len(arc.outbound) 
                                           for arc in self.getLinkingPhrases())/
                                       float(statistics['Arcs']))
        statistics['Avg LP indegree'] = (sum(len(arc.inbound)
                                             for arc in self.getLinkingPhrases())/
                                         float(statistics['Arcs']))
        statistics['Avg LP outdegree'] = (sum(len(arc.outbound)
                                              for arc in self.getLinkingPhrases())/
                                          float(statistics['Arcs']))
        
        statistics['Avg C degree'] = (sum(len(c.inbound) + len(c.outbound) 
                                           for c in self.getNodes())/
                                       float(statistics['Nodes']))
        statistics['Avg C indegree'] = (sum(len(c.inbound)
                                             for c in self.getNodes())/
                                         float(statistics['Nodes']))
        statistics['Avg C outdegree'] = (sum(len(c.outbound)
                                              for c in self.getNodes())/
                                          float(statistics['Nodes']))
        
        return statistics
    
    def __str__(self):
        '''Prints a representation of the graph
        '''
    
        rep =  u'Graph:  %s\n\n' % self.name
    
        for el in self.graphElements.itervalues():
            rep = rep + el.__str__()
    
        rep = rep + u'\n'
        return rep
    
    def generatePhrases(self):
        '''Generates all the phrases from graph and put them in  list'''

        self.phrases = []
        for node in self.getNodes():
            lhConcept = removeLines(node.label)
            lhId = node.id
            for lp in node.outbound:
                linkingPhrase = removeLines(lp.label)
                lpId = lp.id
                for endNode in lp.outbound:
                    rhConcept = removeLines(endNode.label)
                    rhId = endNode.id
                    phrase = Phrase(lhConcept, linkingPhrase, rhConcept)
                    phrase.lhConceptId = lhId
                    phrase.linkingPhraseId = lpId
                    phrase.rhConceptId = rhId
                    self.phrases.append(phrase)
    
    
class GraphElement(object):
    '''A graph element. May be both an arc or a node.
    '''
  
    def __init__(self, graph, elId=None):
    
        if elId == None:
            self.id = graph._getNewId()
        else:
            self.id = elId
        self.graph = graph
        self.label = u''
        self.inbound = []
        self.outbound = []
        self.specificInfo = None
        self.graphicalProperties = None
    
    def _ConnectTo(self, to):
        'Connects to another element'
        raise NotImplementedError
    
    def __str__(self):
        '''
        Outputs a simple representation of this element.
        '''
    
        rep = u'%s named %s, with id %s\n' % (self.__class__.__name__,
                                              self.label, self.id)
    
        rep += u'Inbound elements:\n'
        for el in self.inbound:
            rep += u'  %s, id %s\n' % (el.label, el.id)
      
        rep += u'Outbound elements:\n'
        for el in self.outbound:
            rep += u'  %s, id %s  \n' % (el.label, el.id)

        rep += u'Graphical properties:\n'
        if self.graphicalProperties == None:
            rep += u'   None'
        else:
            rep += str(self.graphicalProperties)
        return rep

    
class Node(GraphElement):
    '''A node in a graph
    '''
  
    def _ConnectTo(self, to):
        'Connect the element to another one'
        if to.__class__ == Arc:
            self.outbound.append(to)
            to.inbound.append(self)
        else:
            raise ValueError('Wrong class instance (expecting Arc, got %s)' %
                             to.__class__.__name__)

                  
class Arc(GraphElement):
    '''An arc in a graph
    '''

    def _ConnectTo(self, to):
        'Connect the element to another one'
        if to.__class__ == Node:
            self.outbound.append(to)
            to.inbound.append(self)
        else:
            raise ValueError('Wrong class instance (expecting Node, got %s)' %
                             to.__class__.__name__)
          
    def connectionsFrom(self):
        'Returns the elements connected to this one'
        for concept in self.inbound:
            yield concept
  
    def connectionsTo(self):
        'Returns the elements connected to this one'
        for concept in self.outbound:
            yield concept

            
class Connection(GraphElement):
    '''A connection between an arc and a node in the graph
    '''

    def _ConnectTo(self, to):
        'Connect the element to another one'
        raise NotImplementedError('This method does not make sense')
          
    def connectionBetween(self, fromEl, to):
        'Connect the element to another one'
        self.outbound.append(to)
        to.inbound.append(fromEl)
          
    def connectionsFrom(self):
        'Returns the elements connected to this one'
        for concept in self.inbound:
            yield concept
  
    def connectionsTo(self):
        'Returns the elements connected to this one'
        for concept in self.outbound:
            yield concept
  

class Point:
    '''A 2D point
    '''
  
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

        
class SpecificInfoContainer:
    '''
    A generic container for format-specific info imported with the graph.
    '''
    
    def __init__(self):
        pass
  
  
if __name__ == '__main__':

    # Creates a simple graph
    g = Graph('Test graph')
  
    n1 = g.addNode('Node 1')
    n2 = g.addNode('Node 2')
    n3 = g.addNode('Node 3', 'ref node 3') # Use an user defined Id
    a1 = g.addArc('Arc 1')  
    a2 = g.addArc('Arc 2')
  
    g.connect(n1, a1)
    g.connect(a1, n2)
    g.connect(a1, n3)
    g.connect(n2, a2)
    g.connect(a2, n3)
  
    print g
  
    # Output should be:
    out = '''
Graph:  Test graph

Node named Node 1, with id 1
Inbound elements:
Outbound elements:
  Arc 1, id 3
Node named Node 2, with id 2
Inbound elements:
  Arc 1, id 3
Outbound elements:
  Arc 2, id 4
Arc named Arc 1, with id 3
Inbound elements:
  Node 1, id 1
Outbound elements:
  Node 2, id 2
  Node 3, id ref node 3
Node named Node 3, with id ref node 3
Inbound elements:
  Arc 1, id 3
  Arc 2, id 4
Outbound elements:
Arc named Arc 2, with id 4
Inbound elements:
  Node 2, id 2
Outbound elements:
  Node 3, id ref node 3

  
  '''
  
  
